/**
 * Copyright (c) 2007, 2008  Collaborative Development Group, C.S. Dept., University of Bari
 *
 * All rights reserved. This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License v1.0  which accompanies this distribution, and is 
 * available at http://www.eclipse.org/legal/epl-v10.html
 */
package it.uniba.di.cdg.penelope.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * Collection of useful reflection utilities.
 */
public class ReflectionUtilities {
	/**
	 * Adapter method for {@link Class#getMethod(String, Class...)}: the API just modifies the contract to throw an unchecked
	 * exception if the method is not found. 
	 * @param targetType the target class
	 * @param methodName the method name
	 * @param parameterTypes (optional) parameter types
	 * @return {@link ReflectionException} if the method is not found
	 */
	public static Method getMethod( Class<?> targetType, String methodName, Class<?> ... parameterTypes ) {
		try {
			return targetType.getDeclaredMethod( methodName, parameterTypes );
		} catch (Exception e) {
			throw new ReflectionException( e );
		}
	}
	
	/**
	 * Returns the unproxied type, if the passed class is a proxy manufactured by Guice or the same type
	 * if it is a clean POJO.
	 * @param type
	 * @return
	 */
	public static Class<?> getUnproxiedType( Class<?> type ) {
		if (type.getName().contains( "$$EnhancerByGuice$$" )) {
			return type.getSuperclass();
		}
		return type;
	}

	/**
	 * Retrieves all the declared methods that have been annotated with the given annotation
	 * @param targetClass the class to search for annotated methods
	 * @param annotationClass the annotation to match
	 * @return the methods (maybe empty)
	 */
	public static Method[] getAnnotatedMethods( Class<?> targetClass, Class<? extends Annotation> annotationClass ) {
		targetClass = getUnproxiedType( targetClass );
		List<Method> methods = new ArrayList<Method>();
		
		Method[] allMethods = targetClass.getDeclaredMethods();
		for (Method m : allMethods) {
			if (m.isAnnotationPresent( annotationClass )) {
				methods.add( m );
			}
		}
		return methods.toArray( new Method[methods.size()] );
	}

	/**
	 * Searches for all the fields annotated with a given annotation.
	 * @param targetClass
	 * @param annotationClass
	 * @return
	 */
	public static Field[] getAnnotatedFields( Class<?> targetClass, Class<? extends Annotation> annotationClass ) {
		targetClass = getUnproxiedType( targetClass );
		List<Field> fields = new ArrayList<Field>();
		
		Field[] allFields = targetClass.getDeclaredFields();
		for (Field f : allFields) {
			if (f.isAnnotationPresent( annotationClass )) {
				fields.add( f );
			}
		}
		return fields.toArray( new Field[fields.size()] );
	}

	/**
	 * @param target
	 * @param string
	 * @param fieldValue
	 */
	public static void setProperty( Object target, String propertyName, Object propertyValue ) {
		Class<?> targetClass = getUnproxiedType( target.getClass() );
		
		try {
			Field field = targetClass.getDeclaredField( propertyName );
			boolean isAccessible = field.isAccessible();
			try {
				field.setAccessible( true );
				field.set( target, propertyValue );
			} finally {
				field.setAccessible( isAccessible );
			}
		} catch (Throwable t) {
			System.err.println( "Property not found!" );
		} 
	}

	/**
	 * @param target
	 * @param propertyName
	 * @return
	 */
	@SuppressWarnings( "unchecked" )
	public static <T> T getProperty( Object target, String propertyName ) {
		final Class<?> targetClass = getUnproxiedType( target.getClass() );
		Object value = null;
		try {
			Field field = targetClass.getDeclaredField( propertyName );
			boolean isAccessible = field.isAccessible();
			try {
				field.setAccessible( true );
				value = field.get( target );
			} finally {
				field.setAccessible( isAccessible );
			}
		} catch (Throwable t) {
			System.err.println( "Property not found!" );
		} 
		return (T) value;
	}
	
	
	public static boolean typeOf( Object target, Class<?> typeToCheck ) {
		final Class<?> targetClass = getUnproxiedType( target.getClass() );
		return typeToCheck.isAssignableFrom( targetClass );
	}

	/**
	 * @param class1
	 * @param class2
	 * @return
	 */
	public static <T extends Annotation> T scanAnnotation( Class<?> type, Class<T> annotationClass ) {
		type = getUnproxiedType( type );
		boolean found = false;
		while (!found && !type.isAnnotationPresent( annotationClass ) && type.getSuperclass() != null) {
			Class<?>[] interfaces = type.getInterfaces();
			for (Class<?> anInterface : interfaces) {
				if (scanAnnotation( anInterface, annotationClass ) != null) {
					found = true;
					type = anInterface;
					break;
				}
			}
			if (!found) {
				type = type.getSuperclass();
			}
		}
		return type != null? type.getAnnotation( annotationClass ) : null;
	}

	/**
	 * @param string
	 * @param i
	 * @return
	 */
	@SuppressWarnings( "unchecked" )
	public static <T> T invokePrivate( Object target, String methodName, Object ...parameters ) {
		Class<?>[] parameterTypes = null;
		if (parameters != null && parameters.length > 0 ) {
			parameterTypes = new Class<?>[parameters.length];
			for (int i = 0; i < parameters.length; i++) {
				parameterTypes[i] = parameters[i].getClass();
			}
		}
		
		Method method = getMethod( target.getClass(), methodName, parameterTypes );
		try {
			method.setAccessible( true );
			return (T) method.invoke( target, parameters );
		} catch (Exception e) {
			throw new ReflectionException( e );
		} finally {
			method.setAccessible( false );
		}
	}
}
